Skip to content

Use the async Always Encrypted key store APIs on async execution paths - #4585

Open
cheenamalhotra wants to merge 24 commits into
mainfrom
dev/cheena/laughing-waffle
Open

cheenamalhotra wants to merge 24 commits into
mainfrom
dev/cheena/laughing-waffle

Conversation

@cheenamalhotra

@cheenamalhotra cheenamalhotra commented Aug 24, 2026 •

Copy link
Copy Markdown
Member

Description

SqlColumnEncryptionKeyStoreProvider gained async APIs in #4540, but the driver did not call them. This PR wires those APIs into SqlCommand parameter-encryption and enclave-package key loading, so asynchronous command execution can await key-store I/O.

Async utility layer

Adds async counterparts to SqlSecurityUtility.DecryptSymmetricKey, GetKeyFromLocalProviders, VerifyColumnMasterKeySignature, AlwaysEncrypted.SymmetricKeyCache.GetKey, and enclave-package key loading/generation.

Async cold-cache misses use a per-key gate and cache recheck to avoid duplicate unwraps. Provider invocation shares the existing synchronous-provider gate, preserving serialization for legacy providers whose base async method calls synchronous code. The returned task is awaited outside that gate, allowing outstanding async I/O for different keys to overlap. Cache publication uses a separate short in-memory lock; sync/async races retain first-published key identity.

Validation, provider error wrapping, cache behavior, and multi-key fallback are preserved. No public API changes. Synchronous callers retain synchronous provider dispatch.

Call site integration

Sync and async describe-parameter-encryption paths share one parser. ReadDescribeEncryptionParameterResultsCore records signature verifications and key decryptions in PendingColumnEncryptionKeyOperations; sync and async wrappers then execute that work after parsing.

Enclave-package generation and post-describe execution gain async counterparts using shared validation and package-building helpers. Deferred retryable package faults use the existing cache/session invalidation and bounded retry handler.

Cancellation

Reader, NonQuery, and XML async entry points capture the caller token, pass it to key-store operations, and clear it during execution cleanup. Cancellation after describe-phase state release avoids accessing a released state object. Cancellation takes precedence over candidate/CMK validation and provider errors. CMK verification checks cancellation before accepting true/false results or publishing signature-cache entries. Successful key decryption still publishes its result when cancellation arrives afterward.

Query metadata cache

Cache lookup is split into an in-memory probe and an awaitable key-loading completion. Async completion is dispatched to a worker so synchronously completed provider tasks cannot run the stale-cache describe fallback on the caller thread. Sync lookup uses the same probe without changing its dispatch behavior.

Notes for reviewers

  • usedCache remains conservatively over-reported on stale-key describe fallback, preserving the existing single retry for conversion errors.
  • Task.Run is deliberate at boundaries that can issue blocking TDS work, including cached-key completion when providers complete synchronously.
  • Provider errors retain their existing wrapping; caller cancellation remains an OperationCanceledException.
  • Human review threads remain open after replies, as requested.

Behaviour notes worth a careful look

Exception ordering on malformed describe results. Parsing finishes before key-store work. If results are malformed and contain an invalid key, the parsing error can now surface first.

Network callback threads. Describe-result and post-describe work retain their worker dispatches rather than performing blocking TDS work on SNI callbacks.

Sequential key loading. Distinct metadata entries can share a key; sequential loading lets the first result warm the cache without duplicate provider I/O.

Scope

This PR covers SqlCommand parameter-encryption and enclave-package key loading, including Reader, NonQuery, and XML async execution. Async enclave attestation is separate (#4886/#4887); those branches were not imported. Session lookup may still perform synchronous attestation I/O, as the enclave-package remarks state.

SqlBulkCopy.WriteToServerAsync still reaches synchronous key decryption through CopyBatchesAsync / TdsParser.LoadColumnEncryptionKeys. Those implementations are unchanged and remain deferred work under #3672.

Issues

Part of #3672, following #4540.

Testing

Regression coverage includes sync/async provider dispatch, multi-key fallback, cancellation and command reuse, CMK signature-cache cancellation, metadata-cache completion, deferred package retry outcomes, same-key cold-cache deduplication, independent keys, and sync/async publication races.

Latest fix 284463de5 restores legacy synchronous-provider serialization without holding the gate across outstanding async I/O. Both new regression cases failed before the fix and pass afterward: a distinct-key waiter cancels without entering the blocked synchronous provider, then succeeds on retry. Earlier 3ec157d48 moved intentionally blocking test-provider work to a dedicated thread and finally-owned release, preserving completion and identity assertions.

Local validation on 284463de5:

dotnet test src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj --framework net10.0 --no-restore --filter 'FullyQualifiedName~SqlCommandAsyncShould|FullyQualifiedName~SqlQueryMetadataCacheAsyncShould|FullyQualifiedName~SqlSecurityUtilityAsyncShould|FullyQualifiedName~EnclaveDelegateAsyncShould|FullyQualifiedName~SqlColumnEncryptionKeyStoreProviderAsyncShould|FullyQualifiedName~AeadAes256CbcHmac256EncryptionKeyTest|FullyQualifiedName~SymmetricKeyTest|FullyQualifiedName~AeadAes256CbcHmac256FactoryTest|FullyQualifiedName~EncryptionAlgorithmFactoryListTest|FullyQualifiedName~InternalsVisibleToTest' --verbosity quiet
dotnet test src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj --framework net10.0 --configuration Release --no-restore --filter 'FullyQualifiedName~SqlCommandAsyncShould|FullyQualifiedName~SqlQueryMetadataCacheAsyncShould|FullyQualifiedName~SqlSecurityUtilityAsyncShould|FullyQualifiedName~EnclaveDelegateAsyncShould|FullyQualifiedName~SqlColumnEncryptionKeyStoreProviderAsyncShould|FullyQualifiedName~AeadAes256CbcHmac256EncryptionKeyTest|FullyQualifiedName~SymmetricKeyTest|FullyQualifiedName~AeadAes256CbcHmac256FactoryTest|FullyQualifiedName~EncryptionAlgorithmFactoryListTest|FullyQualifiedName~InternalsVisibleToTest' --verbosity quiet

Passed: 87 tests in Debug and 87 in Release.

dotnet build src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj --framework net462 -p:TargetOs=Windows_NT --no-restore --verbosity quiet
dotnet build src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj --framework net462 -p:TargetOs=Windows_NT --no-restore --verbosity quiet

Both passed with zero warnings/errors.

Manual run on earlier head 3b10197ce:

dotnet test src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj --framework net10.0 --no-build --no-restore --filter 'FullyQualifiedName~AsyncKeyStoreProviderTests|FullyQualifiedName~TestRetryWhenAEEnclaveCacheIsStale' --verbosity quiet

All eight cases were discovered and skipped because local AE configuration is unavailable. Server-side enclave retry validation remains outstanding. A prior full local unit run aborted in simulated-routing teardown; the isolated case also hangs on unchanged main 616aeff04. No routing tests were changed or disabled.

Current-head CI build 188785, through merge 995d74c410171d423366b766df90cf67a515a2af: all three unit and all three AE tasks passed. Linux Azure manual tests encountered schema-discovery deadlocks; Linux localhost manual tests encountered unsupported distributed-transaction promotion in a pool-reuse test. One failed-job-only Linux retry is in progress; passing jobs were not rerun.

Build 188783 exhausted its two automatic Windows Azure net462 manual-job retries. All failures were schema-discovery deadlocks in AddTVPsToDataTypesTableAsync, including vector metadata tests using GetSchema. These unchanged paths do not invoke key providers. The user explicitly requested leaving these failures outside this PR; no unrelated fixes, weaker assertions, or further retries were applied.

The milestone remains 8.1.0-preview1 by explicit user decision. Main's active line and SqlClientNextVersion remain 8.0.0-preview1, so the two milestone checks remain intentionally blocked. No milestone, release version, or check configuration was changed to bypass the gate.

Guidelines

Please review the contribution guidelines before submitting a pull request:

cheenamalhotra and others added 3 commits August 21, 2026 17:04
Async command execution now resolves Always Encrypted key material through
the asynchronous key store provider APIs, so a provider that performs network
I/O (for example Azure Key Vault) no longer blocks a thread pool thread for the
duration of every column encryption key decryption, column master key signature
verification, and enclave package generation.

Utility layer (Phase 4):
* SqlSecurityUtility gains DecryptSymmetricKeyAsync, GetKeyFromLocalProvidersAsync
  and VerifyColumnMasterKeySignatureAsync.
* SqlSymmetricKeyCache gains GetKeyAsync, which never holds the cache lock across
  provider I/O.

Call sites (Phase 5):
* sp_describe_parameter_encryption result parsing is now shared by the sync and
  async paths. Signature verifications and key decryptions are recorded while
  parsing and executed afterwards, so no key store call is made while the reader
  is still positioned mid-stream.
* The two callback-based describe-parameter-encryption continuations collapse
  into a single async method, preserving the existing thread pool hand-off and
  error/cancellation semantics.
* EnclaveDelegate and SqlCommand gain async enclave package generation, used by
  the async execution path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… store calls

The async key store provider APIs all accept a CancellationToken, but the
Always Encrypted call sites had no way to reach the token supplied to
ExecuteReaderAsync / ExecuteNonQueryAsync / ExecuteXmlReaderAsync, so key
store I/O (for example an Azure Key Vault round trip) could not be cancelled.

Capture the token in a per-execution field on SqlCommand, set at each async
entry point and cleared by the matching cleanup continuation, and pass it to
the describe-parameter-encryption key operations and to enclave package
generation. A field is used rather than signature plumbing because the token
would otherwise have to be threaded through several sync-only overloads of
RunExecuteReader and PrepareForTransparentEncryption.

Also add a manual test that registers a recording key store provider and
asserts that the synchronous execution paths use the synchronous provider
API, the asynchronous paths use the asynchronous provider API, and that
cancelling the caller's token cancels the key store operation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
On a query metadata cache hit the driver still loaded every column encryption
key synchronously, on the caller's thread, and then generated the enclave
package with the synchronous key store provider API. Both can perform key
store network I/O, so an ExecuteReaderAsync that hit the metadata cache could
still block the caller.

Split the cache lookup into an in-memory probe (TryGetCachedQueryMetadata),
which matches the cached cipher metadata onto the parameters and reports which
keys still need decrypting, and a completion step that loads those keys. The
synchronous lookup is now expressed in terms of the same probe, so its
behaviour, including the stale key fallback and the cache hit/miss counters, is
unchanged.

Asynchronous execution now takes the probe and returns a task for the key
loads, which means the cache hit path produces a describe-parameter-encryption
task exactly like the cache miss path does. As a result it also picks up the
asynchronous enclave package generation and no longer issues the command's TDS
write from the caller's thread.

When the cached key information turns out to be stale, that task falls back to
a full describe parameter encryption round trip. The command still reports
usedCache = true in that case because the caller has already returned; this is
deliberately conservative, since over-reporting a cache hit can only cost one
extra retry of an already failing execution, whereas under-reporting it would
suppress the retry a genuine cache hit needs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
Copilot AI lite review requested due to automatic review settings August 24, 2026 17:23
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Aug 24, 2026
@cheenamalhotra cheenamalhotra added this to the 8.0.0-preview1 milestone Aug 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires the async SqlColumnEncryptionKeyStoreProvider APIs into the Always Encrypted async execution paths so ExecuteReaderAsync / ExecuteNonQueryAsync / ExecuteXmlReaderAsync no longer block threads on key store network I/O (e.g., Azure Key Vault), while preserving sync behavior and error semantics.

Changes:

  • Adds async counterparts in the Always Encrypted utility/enclave layers (e.g., SqlSecurityUtility.DecryptSymmetricKeyAsync, SqlSymmetricKeyCache.GetKeyAsync, EnclaveDelegate.GenerateEnclavePackageAsync).
  • Refactors describe-parameter-encryption processing to share parsing between sync/async and to defer key-store operations until after result-set parsing.
  • Flows the caller’s CancellationToken into AE key-store operations via per-execution state on SqlCommand, and updates query metadata cache to split probe vs. async completion.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs Adds async key decryption + CMK signature verification paths with cancellation and consistent wrapping.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs Adds GetKeyAsync with lock-release around provider I/O to avoid blocking sync callers.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs Splits cache lookup into in-memory probe + awaitable completion that loads CEKs asynchronously.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs Introduces per-execution _asyncExecutionCancellationToken for async AE flows.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs Refactors describe-parameter-encryption processing to defer key-store ops and add async completion.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs Adds async enclave package generation and replaces continuation chains with async/await + Task.Run dispatch.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.NonQuery.cs Captures/clears per-execution cancellation token for async non-query execution.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Xml.cs Captures/clears per-execution cancellation token for async XML reader execution.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs Adds async key resolution for enclave package generation and shares key validation/projection.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.Crypto.cs Adds GenerateEnclavePackageAsync and shared helper for building the enclave package.
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlSecurityUtilityAsyncShould.cs Adds unit tests covering async decryption/signature verification/cancellation/cache behavior.
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/EnclaveDelegateAsyncShould.cs Adds unit tests asserting enclave async path uses async provider APIs and honors cancellation.
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs Adds unit tests for async metadata cache completion and stale-key degradation.
src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AsyncKeyStoreProviderTests.cs Adds manual integration test verifying sync vs async provider API usage and token flow.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

GetParameterEncryptionDataReader returned void with an out Task and did
nothing but dispatch asynchronous work, so its name gave no hint that the
caller was receiving a pending operation. The name was inherited from main,
where it was actively misleading: the overload without the Async suffix was
the one that continued from a pending network write, while the overload with
the suffix was the one used when that write had already completed.

Rename the dispatcher to GetParameterEncryptionDataReaderAsync and have it
return the task directly instead of through an out parameter, and rename the
async body to ConsumeDescribeParameterEncryptionResultsAsync, which describes
what it actually does. Both names now carry the Async suffix and both return
a task.

Also make the Task.Run rationale precise. It is not merely a convenience: the
body issues blocking TDS reads before it reaches a suspension point, and
PrepareForTransparentEncryption runs synchronously on the caller's thread
under the async entry points, so awaiting inline would put those reads on the
caller's thread whenever the network write is null or already complete.
Neither continuation this replaced could run inline, since one used Task.Run
and the other used ContinueWith without ExecuteSynchronously.

No behaviour change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
Copilot AI review requested due to automatic review settings August 24, 2026 17:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs:160

  • _asyncExecutionCancellationToken is documented as representing the token “currently in flight”, but it looks like it’s only cleared in the async cleanup continuations. If an async entry point throws during setup (e.g., Task.Factory.FromAsync throws before the continuation runs), the token can remain set on the SqlCommand instance, unnecessarily retaining the caller’s CancellationTokenSource and violating the stated invariant. Consider clearing this field in the synchronous exception paths of InternalExecuteReaderAsync / InternalExecuteNonQueryAsync / InternalExecuteXmlReaderAsync as well (before returning a faulted Task).
        /// <summary>
        /// Cancellation token supplied by the caller of the asynchronous execution that is currently in
        /// flight, or <see cref="CancellationToken.None"/> when the command is executing synchronously.
        /// </summary>
        /// <remarks>

Report CreateColumnEncryptionKeyInfo as the throwing member in the
internal null-argument diagnostics. The throws moved into that shared
helper and are now reachable from both the sync and async decrypt
paths, so naming GetDecryptedKeysToBeSentToEnclave was misleading. This
also matches the convention used by EncryptBytePackage and
ComputeQueryStringHash in the same type.

Use TdsEnums.AEAD_AES_256_CBC_HMAC_SHA256 and
SqlClientEncryptionType.Deterministic in the query metadata cache tests
instead of bare literals with explanatory comments.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
Copilot AI review requested due to automatic review settings August 24, 2026 17:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (4)

Previously missed (1) — in code that hasn't changed since the last review.

src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs:90

  • SqlCommand (and its underlying SqlConnection) should be disposed in this test to avoid leaking resources across the unit test process; use a using declaration for the cached command instance.

This issue also appears in the following locations of the same file:

  • line 113
  • line 137
  • line 150
            TestKeyStoreProvider provider = new TestKeyStoreProvider { PlaintextKey = NewPlaintextKey(seed: 41) };
            SqlCommand command = NewCachedCommand(provider);

src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs:115

  • This test creates a SqlCommand with an owned SqlConnection but never disposes it; prefer a using declaration so the connection/command are cleaned up even if the test fails.
            };
            SqlCommand command = NewCachedCommand(provider);

src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs:152

  • Use a using declaration for the SqlCommand created by NewCachedCommand so the command/connection are disposed deterministically.
            TestKeyStoreProvider provider = new TestKeyStoreProvider { PlaintextKey = NewPlaintextKey(seed: 43) };
            SqlCommand command = NewCachedCommand(provider);

src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlQueryMetadataCacheAsyncShould.cs:139

  • Dispose the SqlCommand created for this test (it owns a SqlConnection) to avoid resource leakage across the unit test run.
            TestKeyStoreProvider provider = new TestKeyStoreProvider { PlaintextKey = NewPlaintextKey(seed: 47) };
            SqlCommand command = NewCachedCommand(provider);

A pass over the async Always Encrypted work looking for correctness,
performance, and consistency problems. The behavioural changes:

Capture the cancellation token on the originating thread. The token lives
in a field so it does not have to be threaded through layers of
callback-driven code that have no use for it, but the Always Encrypted
work is handed to the thread pool and can outlive the cleanup
continuation that resets that field. Reading it late could silently
observe CancellationToken.None. It is now read once at the two
synchronous capture points and passed on as an explicit parameter.

Make the symmetric key cache insert non-cancellable. Cancelling between
a successful decrypt and the cache insert threw away a key that had
already cost a key store round trip, so the next caller paid for it
again.

Drop a dead serverName parameter from both GetDecryptedKeysToBeSentToEnclave
overloads, and give the result list a capacity hint.

The rest is allocation and clarity work: lazily allocate the pending key
operation lists and hand them out as IReadOnlyList, walk them by index so
the interface enumerator is not boxed, and make IsEnclaveEnabled an
explicit field on ColumnMasterKeySignatureVerification rather than a
hard-coded true that only happens to be correct because of where the call
sits.

Also documents three decisions that look like missed optimisations but
are not. The Task.Run in GetParameterEncryptionDataReaderAsync cannot be
awaited inline because the task it waits on is completed by the SNI
network callback, so an inline continuation would run blocking TDS reads
on a callback thread. Column encryption key decryption stays sequential
because distinct cipher metadata entries often share a key, and
GetKeyAsync does not hold its gate across provider I/O, so parallel
misses for the same key would each issue their own key store call.
Sequential decryption lets the first result warm the cache.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
Copilot AI review requested due to automatic review settings August 24, 2026 18:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

CompleteCachedQueryMetadataAsync treated every exception from the column
encryption key load as evidence that the cached metadata was stale, and
invalidated the entry. Cancellation is not evidence of anything of the
sort, so cancelling an execution threw away a perfectly good cache entry
and made the next caller pay for another describe parameter encryption
round trip.

Cancellation now propagates without touching the cache. This failure
mode cannot arise on the synchronous path, so only the asynchronous
overload handles it and the two stay otherwise identical.

The existing cancellation test only asserted that the operation threw,
which is why this went unnoticed. It now also asserts that the entry
survives and that a subsequent uncancelled load still succeeds, and it
fails without the fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 82aca8ca-2078-4a47-860c-6f6ee78cbb02
Copilot AI review requested due to automatic review settings August 24, 2026 19:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs:155

  • The comment says the provider exception will be “simply bubbled up”, but this method wraps non-cancellation failures in SQL.KeyDecryptionFailed. Please update the comment so it matches the actual error behavior (only OperationCanceledException is propagated unwrapped).
            // Decrypt the CEK
            // We will simply bubble up the exception from the DecryptColumnEncryptionKeyAsync function.
            byte[] plaintextKey;

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs:412

  • The comment says the provider exception will be “simply bubbled up”, but this helper wraps non-cancellation failures in SQL.KeyDecryptionFailed. Please update the comment so it matches the actual behavior (only OperationCanceledException is propagated unwrapped).
            // Decrypt the CEK
            // We will simply bubble up the exception from the DecryptColumnEncryptionKeyAsync function.
            byte[] plaintextKey;

@cheenamalhotra cheenamalhotra moved this from To triage to In progress in SqlClient Board Aug 24, 2026
cheenamalhotra added a commit that referenced this pull request Aug 25, 2026
The required sqlclient-pr pipeline is failing only on tests unrelated to
this change, in a pipeline with a ~48% baseline failure rate across other
PRs. Observed failures, none of which touch any file in this PR:

  MARSTest.MarsScenarioClientJoin              (also failed on PR #4567)
  SqlCommandCancelTest.TimeOutDuringRead_Tcp   (timing sensitive)
  TransactionEnlistmentTest.TestManualEnlistment_Enlist
                                               (also failed on PR #4585)

The /azp run comment trigger is not enabled on this repo, so refreshing
the head SHA is the only available way to re-run the required checks.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cfc64bc6-a9e6-490d-88b1-4b78d25aa103

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

A moderate cancellation-handling issue remains, while full validation and enclave-specific integration coverage are incomplete.

No open issues 🎉

1 resolved since last review

Review effort: Lite

@priyankatiwari08 priyankatiwari08 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • GetKeyAsync doesn't dedupe cold-cache misses, so parallel async executes trigger one key-store unwrap each.

@github-project-automation github-project-automation Bot moved this from In progress to Waiting for customer in SqlClient Board Oct 26, 2026
Gate async loads per cache key, recheck after waiting, and remove idle gates. Preserve independent cancellation, different-key concurrency, and sync/async publication behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 038a9db2-f9a4-45c3-a80a-73cfe8a3bc9f
Copilot AI lite review requested due to automatic review settings October 26, 2026 17:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Four unresolved moderate-severity findings affect cancellation behavior and async concurrency.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Preserve cancellation when the final candidate fails

src/​Microsoft.Data.SqlClient/​src/​Microsoft/​Data/​SqlClient/​SqlSecurityUtility.cs:329

issue (P2, async): Preserve cancellation when the final candidate fails

If the provider for the last (or only) candidate cancels the caller token but returns a normal exception, the catch records that exception and the loop ends; this branch then rethrows lastException without another cancellation check. That masks caller cancellation as SQL.KeyDecryptionFailed/a validation error, unlike the documented async contract and the multi-candidate case handled by the next-iteration check. Recheck cancellationToken immediately before rethrowing the final candidate error.

This issue also appears on line 617 of the same file.

Review effort: Lite

Observe caller cancellation before rethrowing the last CEK candidate error or wrapping a CMK verification exception. Cover immediate and deferred failures with single and multiple candidates.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 038a9db2-f9a4-45c3-a80a-73cfe8a3bc9f
Copilot AI lite review requested due to automatic review settings October 26, 2026 18:01
@cheenamalhotra

Copy link
Copy Markdown
Member Author

Preserve cancellation when the final candidate fails

Fixed the review-body finding in 2b5aabd: cancellation now wins before rethrowing the final CEK error and before wrapping a CMK verification exception. Six immediate/deferred failure regressions failed before the fix; all 85 focused tests now pass in Debug and Release.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The changes span asynchronous encryption, caching, cancellation, and enclave execution paths, while new-head CI and enclave-backed retry validation remain pending.

No open issues 🎉

Review effort: Lite

Keep synchronous provider blocking off the thread pool and let the test orchestration own its release without a competing provider watchdog.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 038a9db2-f9a4-45c3-a80a-73cfe8a3bc9f
Copilot AI lite review requested due to automatic review settings October 26, 2026 19:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Same-key async key loads must bypass the per-key gate when CEK caching is disabled.

No open issues 🎉

Review effort: Lite

@priyankatiwari08 priyankatiwari08 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor concerns:

  • Async CEK decrypt is no longer serialized under s_cacheLock (inline).

Serialize provider invocation with the existing semaphore, releasing it before awaiting returned tasks. Cover synchronous and base-async owners plus cancellation while waiting without blocking completed-key publication.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 038a9db2-f9a4-45c3-a80a-73cfe8a3bc9f
Copilot AI lite review requested due to automatic review settings November 3, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad, high-risk async command and enclave state changes lack current-head CI and enclave-enabled integration validation, leaving end-to-end retry, cancellation, and cleanup behavior unverified.

No open issues 🎉

Review effort: Lite

@priyankatiwari08 priyankatiwari08 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good — 284463d addresses the async CEK serialization concern.

@cheenamalhotra cheenamalhotra added the Hotfix 8.0.0 PRs that must be backported to release/8.0 label Nov 13, 2026
@cheenamalhotra cheenamalhotra moved this from Waiting for customer to In review in SqlClient Board Nov 13, 2026
@cheenamalhotra cheenamalhotra modified the milestone: 8.1.0-preview1 Nov 13, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Hotfix 8.0.0 PRs that must be backported to release/8.0

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

5 participants